// main.rs
use anyhow::{Context, Result, bail};
use base32ct::{Base32UpperUnpadded, Encoding};
use clap::{Parser, Subcommand};
use indicatif::{ProgressBar, ProgressStyle};
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use sevenz_rust2::{ArchiveReader, Password};
use sha3::{Digest, Sha3_256};

use std::{
    fs::{self, File},
    io::{self, BufReader, Read},
    path::{Path, PathBuf},
};
use tokio;

/// ------------------ CLI ------------------
#[derive(Parser)]
#[command(name = "galdrive", version, about)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// 计算单个文件的 CID
    Cid { file: PathBuf },
    /// 为目录或文件生成 Manifest
    Manifest { path: PathBuf },
    /// 将源目录同步到目标目录（按 CID 命名文件）
    Sync { from: PathBuf, to: PathBuf },
}

/// ------------------ 通用类型 ------------------
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
enum Entry {
    File(FileEntry),
    Dir(DirEntry),
}

#[derive(Debug, Serialize, Deserialize)]
struct FileEntry {
    name: String,
    size: u64,
    cid: String,
    /// 对于压缩文件，记录其内部条目
    content: Option<Vec<Entry>>,
}

#[derive(Debug, Serialize, Deserialize)]
struct DirEntry {
    name: String,
    entries: Vec<Entry>,
    //如果文件夹内是一对分割的文件，记录原文件CID
    cid: Option<String>,
}

/// ------------------ 主函数 ------------------
#[tokio::main]
async fn main() -> Result<()> {
    let cli = Cli::parse();
    match cli.command {
        Commands::Cid { file } => {
            let cid = compute_file_cid(&file)?;
            println!("{cid}");
        }
        Commands::Manifest { path } => {
            let entry = if path.is_file() {
                Entry::File(file_entry(&path)?)
            } else {
                Entry::Dir(dir_entry(&path)?)
            };
            println!("{}", serde_json::to_string_pretty(&entry)?);
        }
        Commands::Sync { from, to } => {
            sync_directory(&from, &to).await?;
        }
    }
    Ok(())
}

/// ------------------ CID / 哈希 ------------------
/// 目前仅支持 sha3-256，但预留了 codec 参数方便扩展
const DEFAULT_CODEC: &str = "sha3-256";

fn compute_file_cid(path: &Path) -> Result<String> {
    let hash = file_hash(path)?;
    Ok(format!(
        "{}_{}",
        DEFAULT_CODEC,
        Base32UpperUnpadded::encode_string(&hash)
    ))
}

fn file_hash(path: &Path) -> Result<Vec<u8>> {
    let file = File::open(path)?;
    let file_size = file.metadata()?.len();
    let pb = ProgressBar::new(file_size);
    pb.set_style(
        ProgressStyle::default_bar()
            .template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})").unwrap()
            .progress_chars("#>-"),
    );
    pb.set_message(format!("Hashing {}", path.display()));
    let mut reader = BufReader::new(file);
    let mut hasher = Sha3_256::new();
    let mut buf = [0u8; 1 << 16]; // 64 KiB buffer
    loop {
        let n = reader.read(&mut buf)?;
        if n == 0 {
            break;
        }
        hasher.update(&buf[..n]);
        pb.inc(n as u64);
    }
    pb.finish_with_message(format!("Finished hashing {}", path.display())); // Mark as finished

    Ok(hasher.finalize().to_vec())
}

/// ------------------ Manifest 构建 ------------------
/// 目录条目（递归）
fn dir_entry(path: &Path) -> Result<DirEntry> {
    let name = file_name(path)?;
    let mut entries = Vec::new();

    // 并行读取目录内容
    let child_paths: Vec<_> = fs::read_dir(path)?
        .map(|e| e.map(|v| v.path()))
        .collect::<io::Result<_>>()?;

    // 并行构建子条目
    let child_entries: Result<Vec<Entry>> = child_paths
        .into_par_iter()
        .map(|p| {
            if p.is_dir() {
                dir_entry(&p).map(Entry::Dir)
            } else {
                file_entry(&p).map(Entry::File)
            }
        })
        .collect();
    entries.extend(child_entries?);
    //如果文件夹名称有扩展名.split
    //则计算文件们合并后的数据流的CID
    let cid = if name.ends_with(".split") {
        println!("Wait to compute CID for folder");
        let mut files: Vec<_> = entries
            .iter()
            .filter_map(|e| match e {
                Entry::File(f) => Some(path.join(&f.name)),
                _ => None,
            })
            .collect();
        let mut hasher = Sha3_256::new();
        for f in files.iter() {
            if !f
                .extension()
                .and_then(|f| f.to_str())
                .is_some_and(|e| e.len() == 3 && e.chars().all(|c| c.is_ascii_digit()))
            {
                eprintln!(
                    "Warning: {} is a file with extension .split, but it is not a split file",
                    f.display()
                );
            }
            let file_path = File::open(f)?;

            let mut reader = BufReader::new(file_path);
            let mut buf = [0u8; 1 << 16]; // 64 KiB buffer
            loop {
                let n = reader.read(&mut buf)?;
                if n == 0 {
                    break;
                }
                hasher.update(&buf[..n]);
            }
        }
        let hash = hasher.finalize().to_vec();
        Some(format!(
            "{}_{}",
            DEFAULT_CODEC,
            Base32UpperUnpadded::encode_string(&hash)
        ))
    } else {
        None
    };

    Ok(DirEntry { name, entries, cid })
}

/// 普通/压缩文件条目
fn file_entry(path: &Path) -> Result<FileEntry> {
    let name = file_name(path)?;
    let size = fs::metadata(path)?.len();
    let cid = compute_file_cid(path)?;
    let content = archive_entries(path)?;
    Ok(FileEntry {
        name,
        size,
        cid,
        content,
    })
}

/// 如果文件是支持的压缩格式，则遍历内部文件并计算 CID
fn archive_entries(path: &Path) -> Result<Option<Vec<Entry>>> {
    let mut archive = match ArchiveReader::open(path, Password::default()) {
        Ok(a) => a,
        Err(_) => return Ok(None), // 不是压缩文件或密码错误
    };

    let mut entries = Vec::new();
    archive.for_each_entries(|entry, mut reader| {
        if entry.is_directory {
            return Ok(true);
        }
        println!("Wait to compute CID for file {}", entry.name);
        let mut hasher = Sha3_256::new();
        let mut buf = [0u8; 1 << 16];
        loop {
            let n = reader.read(&mut buf)?;
            if n == 0 {
                break;
            }
            hasher.update(&buf[..n]);
        }
        let hash = hasher.finalize().to_vec();
        let cid = format!(
            "{}_{}",
            DEFAULT_CODEC,
            Base32UpperUnpadded::encode_string(&hash)
        );

        entries.push(Entry::File(FileEntry {
            name: entry.name.to_string(),
            size: entry.size,
            cid,
            content: None,
        }));
        Ok(true)
    })?;

    if entries.is_empty() {
        Ok(None)
    } else {
        Ok(Some(entries))
    }
}

/// ------------------ Sync 逻辑 ------------------
async fn sync_directory(from: &Path, to: &Path) -> Result<()> {
    // 1. 构建源目录 manifest
    let manifest = dir_entry(from)?;
    let manifest_path = from.join("manifest.json");
    fs::write(&manifest_path, serde_json::to_string_pretty(&manifest)?)
        .with_context(|| format!("write manifest {}", manifest_path.display()))?;

    // 2. 目标目录准备
    fs::create_dir_all(to)?;

    // 3. 递归复制
    copy_by_cid(from, to, &manifest.entries)?;

    Ok(())
}

/// 递归复制：按 CID 命名文件
fn copy_by_cid(from: &Path, to: &Path, entries: &[Entry]) -> Result<()> {
    for entry in entries {
        match entry {
            Entry::File(f) => {
                let src = from.join(&f.name);
                let dst = to.join(&f.cid);
                if dst.exists() {
                    // 简单的存在性校验，可改为 CID 校验
                    continue;
                }
                fs::copy(&src, &dst)
                    .with_context(|| format!("copy {} -> {}", src.display(), dst.display()))?;
            }
            Entry::Dir(d) => {
                let sub_from = from.join(&d.name);
                copy_by_cid(&sub_from, to, &d.entries)?;
            }
        }
    }
    Ok(())
}

/// ------------------ 小工具 ------------------
fn file_name(p: &Path) -> Result<String> {
    p.file_name()
        .and_then(|s| s.to_str())
        .map(|s| s.to_owned())
        .with_context(|| format!("invalid file name {}", p.display()))
}
